
import { RGBColor } from '../color/color';

function loadImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = () => reject();
    img.src = url;
  }).then((/** @type {HTMLImageElement} */ img) => {
    let canvas = document.createElement('canvas');
    canvas.width = img.width;
    canvas.height = img.height;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(img, 0, 0);
    return ctx.getImageData(0, 0, img.width, img.height);
  });
}

const COLOR_NONE = new RGBColor(0, 0, 0, 0);

/**
 * @param {IndexImage} t 
 * @param {ImageData} data 
 */
function loadAllAsIndexImage(t, data) {
  const to = t.data;
  const sets = {};
  let count = 0;
  const array = data.data;

  let j = 0;
  for (let i = 0; i < array.length; i += 4, j++) {
    let rgba = (array[i] << 24) + (array[i + 1] << 16) + (array[i + 2] << 8) + array[i + 3];
    let paletteIdx = sets[rgba];
    if (paletteIdx != null) {
      to[j] = paletteIdx;
      continue;
    }
    paletteIdx = count++;
    sets[rgba] = paletteIdx;
    to[j] = paletteIdx;
    t.palette.push(new RGBColor(array[i], array[i + 1], array[i + 2], array[i + 3]));
  }
}

/**
 * @param {IndexImage} t 
 * @param {ImageData} data 
 * @param {number} x 
 * @param {number} y 
 * @param {number} w 
 * @param {number} h 
 */
function loadRegionAsIndexImage(t, data, x, y, w, h) {
  const { width, height } = data;
  if (x >= width || x + w < 0 || y >= height || y + h < 0) {
    t.palette.push(COLOR_NONE);
    return;
  }
  const sets = {};
  let count = 0;
  if (x < 0 || x + w >= width || y < 0 || y + h >= height) {
    t.palette.push(COLOR_NONE);
    sets[0] = 0;
    count++;
  }

  const to = t.data;
  const from = data.data;

  // xstart, xend, ystart, yend, xx, yy, width, height, off1, i 参考系是 from
  // x, y, h, w, j, off2 参考系是 to
  let xstart = Math.max(0, x);
  let xend = Math.min(width, x + w);
  let ystart = Math.max(0, y);
  let yend = Math.min(height, y + h);
  for (let yy = ystart; yy < yend; yy++) {
    let off1 = yy * width;
    let off2 = (yy - y) * w;
    for (let xx = xstart; xx < xend; xx++) {
      let i = (off1 + xx) << 2;
      let j = (off2 + xx - x);

      let rgba = (from[i] << 24) + (from[i + 1] << 16) + (from[i + 2] << 8) + from[i + 3];
      let paletteIdx = sets[rgba];
      if (paletteIdx != null) {
        to[j] = paletteIdx;
        continue;
      }
      paletteIdx = count++;
      sets[rgba] = paletteIdx;
      to[j] = paletteIdx;
      t.palette.push(new RGBColor(from[i], from[i + 1], from[i + 2], from[i + 3]));
    }
  }
}

/**
 * 索引图像
 * 请保证图像中所有颜色不超过 256 种
 */
class IndexImage {
  /**
   * @param {ImageData} data 
   * @param {number} x 
   * @param {number} y 
   * @param {number} width 
   * @param {number} height 
   */
  constructor(data, x = null, y = null, width = null, height = null) {
    /** @type {RGBColor[]} */
    this.palette = [];

    if (x == null) {
      this.width = data.width;
      this.height = data.height;
      this.data = new Uint8Array(data.width * data.height);
      loadAllAsIndexImage(this, data);
    } else {
      this.width = width >= 0 ? (width | 0) : 0;
      this.height = height >= 0 ? (height | 0) : 0;
      const length = this.width * this.height;
      if (length === 0) {
        throw { msg: `illegal width or height`, data: {width, height} };
      }
      this.data = new Uint8Array(length);
      loadRegionAsIndexImage(this, data, (x | 0), (y | 0), this.width, this.height);
    }
  }

  /**
   * @param {(colorIdx:number, toIdx:number) => void} handler
   */
  _mapRegion(handler, x = 0, y = 0, width = this.width, height = this.height) {
    if (x > this.width || x + width < 0 || y > this.height || y + height < 0) {
      return;
    }

    const { data } = this;
    let xstart = Math.max(0, x);
    let xend = Math.min(this.width, x + width);
    let ystart = Math.max(0, y);
    let yend = Math.min(this.height, y + height);
    for (let yy = ystart; yy < yend; yy++) {
      let off1 = yy * this.width;
      let off2 = (yy - y) * width;
      for (let xx = xstart; xx < xend; xx++) {
        let idx1 = off1 + xx;
        let idx2 = (off2 + xx - x);
        handler(data[idx1], idx2);
      }
    }
  }

  toIndexArray(x = 0, y = 0, width = this.width, height = this.height) {
    const dd = new Uint8Array(width * height);
    this._mapRegion((data, idx) => dd[idx] = data, x, y, width, height);
    return dd;
  }

  /**
   * width, height 必须是正数
   * @param {RGBColor[]} palette
   */
  toImageData(palette = this.palette, x = 0, y = 0, width = this.width, height = this.height) {
    const d = new ImageData(width, height);
    const dd = d.data;
    this._mapRegion((colorIdx, idx) => {
      let c = palette[colorIdx];
      let idx2 = idx << 2;
      dd[idx2] = c.r;
      dd[idx2 + 1] = c.g;
      dd[idx2 + 2] = c.b;
      dd[idx2 + 3] = c.a;
    }, x, y, width, height);
    return d;
  }

  getColor(x, y) {
    if (x < 0 || x >= this.width || y < 0 || y >= this.height) return COLOR_NONE.clone();
    let i = this.data[y * this.width + x];
    return this.palette[i].clone();
  }

  createPainter(x = 0, y = 0, width = this.width, height = this.height) {
    return new IndexImagePainter(this, x, y, width, height);
  }
}

class IndexImagePainter {
  /**
   * @param {IndexImage} img
   * @param {number} x 
   * @param {number} y 
   * @param {number} width 
   * @param {number} height 
   */
  constructor(img, x, y, width, height) {
    this.img = img;
    this.palette = img.palette.map(c => c.clone());

    this.x = x || 0;
    this.y = y || 0;
    this.width = width || img.width;
    this.height = height || img.height;
  }

  resetPalette() {
    this.palette = this.img.palette.map(c => c.clone());
  }

  getColor(x, y) {
    if (x < 0 || x >= this.width || y < 0 || y >= this.height) return COLOR_NONE.clone();
    return this.img.getColor(x + this.x, y + this.y);
  }

  toImageData() {
    return this.img.toImageData(this.palette, this.x, this.y, this.width, this.height);
  }

  createPainter(x = 0, y = 0, width = this.width, height = this.height) {
    return this.img.createPainter(x + this.x, y + this.y, width, height);
  }

  to2dCanvas() {
    let c = document.createElement('canvas');
    c.width = this.width;
    c.height = this.height;
    let ctx = c.getContext('2d');
    const data = this.toImageData();
    ctx.putImageData(data, 0, 0);
    return c;
  }

  /**
   * @param {(c:RGBColor) => boolean} predicate
   */
  toPath(predicate, offx = 0, offy = 0) {
    const {width:w, height:h} = this;
    const pdMap = pathDirectionMap;

    // Step 1: Build pdMap --> 0,1 matrix
    const cList = this.palette.map(c => predicate(c) ? 1 : 0);
    const bArray = new Uint8Array(w * h);
    this.img._mapRegion((colorIdx, idx) => bArray[idx] = cList[colorIdx], this.x, this.y, w, h);

    const ww = w + 1;
    const hh = h + 1;
    const dirArray = new Uint8Array(ww * hh);

    // Step 2: Build dirArray --> direction matrix:
    // 1:Left  2:Right  4:Top  8:Bottom

    // dirArray[y][x] = pdMap[8*bArray[y][x] + 4*bArray[y][x-1] + 2*bArray[y-1][x] + bArray[y-1][x-1] ]
    // dirArray[y*ww+x] = pdMap[8*bArray[y*w+x] + 4*bArray[y*w+x-1] + 2*bArray[(y-1)*w+x] + bArray[(y-1)*w+x-1] ]
    // 四个角
    dirArray[0] = pdMap[8*bArray[0]];
    dirArray[w] = pdMap[4*bArray[w-1]];
    dirArray[h*ww] = pdMap[2*bArray[(h-1)*w]];
    dirArray[hh*ww-1] = pdMap[bArray[h*w-1]];
    // 贴边
    // 左: x=0  1<=y<h
    // 右: x=w  1<=y<h
    for (let y = 1; y < h; y++) {
      dirArray[y*ww] = pdMap[8*bArray[y*w] + 2*bArray[(y-1)*w] ];
      dirArray[y*ww+w] = pdMap[4*bArray[y*w+w-1] + bArray[(y-1)*w+w-1] ];
    }
    // 上: 1<=x<w  y=0
    // 下: 1<=x<w  y=h
    for (let x = 1; x < w; x++) {
      dirArray[x] = pdMap[8*bArray[x] + 4*bArray[x-1] ];
      dirArray[h*ww+x] = pdMap[2*bArray[(h-1)*w+x] + bArray[(h-1)*w+x-1] ]
    }
    // 其它 1<=x<w  1<=y<h
    for (let y = 1; y < h; y++) {
      let yOff = y*ww;
      let yOff1 = y*w;
      let yOff2 = (y-1)*w;
      for (let x = 1; x < w; x++) {
        dirArray[yOff+x] = pdMap[8*bArray[yOff1+x] + 4*bArray[yOff1+x-1] + 2*bArray[yOff2+x] + bArray[yOff2+x-1] ];
      }
    }

    // Step 3: Connect all direction vectors
    let i = 0, currentX = -1, currentY = -1, nextX = 0, nextY = 0;
    let dir = 0, distance = 0, len = 0;
    const dirLen = dirArray.length;
    /** @type {string[][]} */ const pathsList = [];
    /** @type {number[]} */ const lengths = [];

    while (true) {
      for (; i < dirLen; i++) {
        if (dirArray[i]) break;
      }
      if (i == dirLen) break;

      nextY = (i / ww) | 0;
      nextX = i % ww;
      let paths = [currentX == -1 ? `M${nextX-offx},${nextY-offy}` : `m${nextX-currentX},${nextY-currentY}`];

      let ii = i;
      let vv = dirArray[ii];
      while (vv) {
        // determine direction
        dir = (vv & 1) ? 1 : (vv & 2) ? 2 : (vv & 4) ? 4 : 8;

        // count distance
        distance = 0;
        while (vv & dir) {
          dirArray[ii] ^= dir;
          switch (dir) {
            case 1: nextX--; break;
            case 2: nextX++; break;
            case 4: nextY--; break;
            case 8: nextY++; break;
          }
          distance++;

          ii = nextY * ww + nextX;
          vv = dirArray[ii];
        }
        len += distance;

        // generate path text
        currentX = nextX;
        currentY = nextY;
        paths.push((dir == 1) ? `h-${distance}` : (dir == 2) ? `h${distance}` :
            (dir == 4) ? `v-${distance}` : `v${distance}`);
      }
      paths.push('Z');
      pathsList.push(paths);
      lengths.push(len);
      len = 0;
    }

    return {
      path : pathsList.map(list => list.join('')).join(''),
      lengths
    };
  }
}

/**
 * key: 左上x1 + 右上x2 + 左上x4 + 右上x8
 * value: 向左=1 向右=2 向上=4 向下=8
 */
const pathDirectionMap = new Uint8Array(16);
(function __(){[0,1,4,1,8,8,12,8,2,3,4,1,2,2,4,0].map((v,i)=>pathDirectionMap[i]=v)}());


export {
  loadImage,
  IndexImage, IndexImagePainter
};
